import { utilities as csUtils } from '@cornerstonejs/core';
import type { Types } from '@cornerstonejs/core';
import type {
  AnnotationCompletedEventType,
  AnnotationModifiedEventType,
  AnnotationRemovedEventType,
} from '../../../types/EventTypes';
import { state as annotationState } from '../../../stateManagement/annotation';
import type AnnotationGroupSelector from '../../../types/AnnotationGroupSelector';
import getInterpolationDataCollection from '../../contours/interpolation/getInterpolationDataCollection';
import type {
  InterpolationViewportData,
  AcceptInterpolationSelector,
} from '../../../types/InterpolationTypes';
import interpolate from '../../contours/interpolation/interpolate';
import deleteRelatedAnnotations from './deleteRelatedAnnotations';
import type { InterpolationROIAnnotation } from '../../../types/ToolSpecificAnnotationTypes';
import ChangeTypes from '../../../enums/ChangeTypes';
import getViewportForAnnotation from '../../getViewportForAnnotation';
import { addContourSegmentationAnnotation } from '../../contourSegmentation/addContourSegmentationAnnotation';

const { uuidv4 } = csUtils;

const ChangeTypesForInterpolation = [
  ChangeTypes.HandlesUpdated,
  ChangeTypes.InterpolationUpdated,
];

export default class InterpolationManager {
  static toolNames = [];

  static addTool(toolName: string) {
    if (!this.toolNames.includes(toolName)) {
      this.toolNames.push(toolName);
    }
  }

  static removeTool(toolName: string) {
    if (this.toolNames.includes(toolName)) {
      this.toolNames = this.toolNames.filter((name) => name !== toolName);
    }
  }

  /**
   * Accepts the autogenerated interpolations, marking them as non-autogenerated.
   * Can provide a selector to choose which ones to accept.
   *
   * Rules for which items to select:
   * 1. Only choose annotations having the same segment index and segmentationID
   * 2. Exclude all contours having the same interpolation UID as any other contours
   *    on the same slice.
   * 3. Exclude autogenerated annotations
   * 4. Exclude any reset interpolationUIDs (this is a manual operation to allow
   *    creating a new interpolation)
   * 5. Find the set of interpolationUID's remaining
   *    a. If the set is of size 0, assign a new interpolationUID
   *    b. If the set is of size 1, assign that interpolationUID
   *    c. Otherwise (optional, otherwise do b for size>1 randomly),
   *       for every remaining annotation, find the one whose center
   *       point is closest to the center point of the new annotation.
   *       Choose that interpolationUID
   *
   * To allow creating new interpolated groups, the idea is to just use a new
   * segment index, then have an operation to update the segment index of an
   * interpolation set.  That way the user can easily draw/see the difference,
   * and then merge them as required.
   * However, the base rules allow creating two contours on a single image to
   * create a separate set.
   */
  static acceptAutoGenerated(
    annotationGroupSelector: AnnotationGroupSelector,
    selector: AcceptInterpolationSelector = {}
  ) {
    const { toolNames, segmentationId, segmentIndex, sliceIndex } = selector;
    for (const toolName of toolNames || InterpolationManager.toolNames) {
      const annotations = annotationState.getAnnotations(
        toolName,
        annotationGroupSelector
      ) as InterpolationROIAnnotation[];
      if (!annotations?.length) {
        continue;
      }
      for (const annotation of annotations) {
        const { interpolationUID, data, autoGenerated, metadata } = annotation;
        if (interpolationUID) {
          annotation.interpolationCompleted = true;
        }
        if (!autoGenerated) {
          continue;
        }
        if (segmentIndex && segmentIndex !== data.segmentation.segmentIndex) {
          continue;
        }
        if (
          sliceIndex !== undefined &&
          metadata &&
          sliceIndex !== metadata.sliceIndex
        ) {
          continue;
        }
        if (
          segmentationId &&
          segmentationId !== data.segmentation.segmentationId
        ) {
          continue;
        }
        addContourSegmentationAnnotation(annotation);
        annotation.autoGenerated = false;
      }
    }
  }

  /**
   * When an annotation is completed, if the configuration includes interpolation,
   * then find matching interpolations and interpolation between this segmentation
   * and the other segmentations of the same type.
   */
  static handleAnnotationCompleted = (evt: AnnotationCompletedEventType) => {
    const annotation = evt.detail.annotation as InterpolationROIAnnotation;
    if (!annotation?.metadata) {
      return;
    }
    const { toolName, originalToolName } = annotation.metadata;

    if (
      !this.toolNames.includes(toolName) &&
      !this.toolNames.includes(originalToolName)
    ) {
      return;
    }

    const viewport = getViewportForAnnotation(annotation);
    if (!viewport) {
      console.warn('Unable to find viewport for', annotation);
      return;
    }
    const sliceData: Types.ImageSliceData = getSliceData(viewport);
    const viewportData: InterpolationViewportData = {
      viewport,
      sliceData,
      annotation,
      interpolationUID: annotation.interpolationUID,
    };
    const hasInterpolationUID = !!annotation.interpolationUID;
    // If any update, triggered on an annotation, then it will be treated as non-autogenerated.
    annotation.autoGenerated = false;
    if (hasInterpolationUID) {
      // This has already been configured with matching details, so just run
      //  the interpolation again.
      deleteRelatedAnnotations(viewportData);
      interpolate(viewportData);
      return;
    }
    const filterData = [
      {
        key: 'segmentIndex',
        value: annotation.data.segmentation.segmentIndex,
        parentKey: (annotation) => annotation.data.segmentation,
      },
      {
        key: 'viewPlaneNormal',
        value: annotation.metadata.viewPlaneNormal,
        parentKey: (annotation) => annotation.metadata,
      },
      {
        key: 'viewUp',
        value: annotation.metadata.viewUp,
        parentKey: (annotation) => annotation.metadata,
      },
    ];
    let interpolationAnnotations = getInterpolationDataCollection(
      viewportData,
      filterData
    );
    // Skip other type of annotation interpolationUID's that are co-located
    const { sliceIndex } = annotation.metadata;
    const skipUIDs = new Set<string>();
    interpolationAnnotations.forEach((interpolationAnnotation) => {
      if (
        interpolationAnnotation.interpolationCompleted ||
        interpolationAnnotation.metadata.sliceIndex === sliceIndex
      ) {
        const { interpolationUID } = interpolationAnnotation;
        skipUIDs.add(interpolationUID);
      }
    });
    interpolationAnnotations = interpolationAnnotations.filter(
      (interpolationAnnotation) =>
        !skipUIDs.has(interpolationAnnotation.interpolationUID)
    );

    // Assign a new interpolationUID (this is checked above, so will be empty initially)
    annotation.interpolationUID =
      interpolationAnnotations[0]?.interpolationUID || uuidv4();
    viewportData.interpolationUID = annotation.interpolationUID;
    interpolate(viewportData);
  };

  /**
   * This method gets called when an annotation changes.  It will then trigger
   * related already interpolated annotations to be updated with the modified data.
   */
  static handleAnnotationUpdate = (evt: AnnotationModifiedEventType) => {
    const annotation = evt.detail.annotation as InterpolationROIAnnotation;
    const { changeType = ChangeTypes.HandlesUpdated } = evt.detail;
    if (!annotation?.metadata) {
      return;
    }
    const { toolName, originalToolName } = annotation.metadata;

    if (
      (!this.toolNames.includes(toolName) &&
        !this.toolNames.includes(originalToolName)) ||
      !ChangeTypesForInterpolation.includes(changeType)
    ) {
      return;
    }
    const viewport = getViewportForAnnotation(annotation);
    if (!viewport) {
      console.warn(
        'Unable to find matching viewport for annotation interpolation',
        annotation
      );
      return;
    }
    if (annotation.autoGenerated) {
      // Dont fire the annotation changed events here, as that leads to recursion,
      // although this is in fact completing the event, so trigger the segmentation add
      addContourSegmentationAnnotation(annotation);
      annotation.autoGenerated = false;
    }

    const sliceData: Types.ImageSliceData = getSliceData(viewport);
    const viewportData: InterpolationViewportData = {
      viewport,
      sliceData,
      annotation,
      interpolationUID: annotation.interpolationUID,
      isInterpolationUpdate: changeType === ChangeTypes.InterpolationUpdated,
    };
    interpolate(viewportData);
  };

  /**
   * Delete interpolated annotations when their endpoints are deleted.
   */
  static handleAnnotationDelete = (evt: AnnotationRemovedEventType) => {
    const annotation = evt.detail.annotation as InterpolationROIAnnotation;
    if (!annotation?.metadata) {
      return;
    }
    const { toolName } = annotation.metadata;

    if (!this.toolNames.includes(toolName) || annotation.autoGenerated) {
      return;
    }
    const viewport = getViewportForAnnotation(annotation);

    if (!viewport) {
      console.warn(
        "No viewport, can't delete interpolated results",
        annotation
      );
      return;
    }

    const sliceData: Types.ImageSliceData = getSliceData(viewport);
    const viewportData: InterpolationViewportData = {
      viewport,
      sliceData,
      annotation,
      interpolationUID: annotation.interpolationUID,
    };
    // If any update, triggered on an annotation, then it will be treated as non-interpolated.
    annotation.autoGenerated = false;
    deleteRelatedAnnotations(viewportData);
  };
}

function getSliceData(viewport): Types.ImageSliceData {
  const sliceData: Types.ImageSliceData = {
    numberOfSlices: viewport.getNumberOfSlices(),
    imageIndex: viewport.getCurrentImageIdIndex(),
  };
  return sliceData;
}
